Skip to content

feat(autoware_carla_interface): publish CARLA traffic-light states, matched to the map by position - #13327

Merged
hakuturu583 merged 11 commits into
autowarefoundation:mainfrom
hakuturu583:feat/carla-traffic-light-position-match
Sep 8, 2026
Merged

feat(autoware_carla_interface): publish CARLA traffic-light states, matched to the map by position#13327
hakuturu583 merged 11 commits into
autowarefoundation:mainfrom
hakuturu583:feat/carla-traffic-light-position-match

Conversation

@hakuturu583

@hakuturu583 hakuturu583 commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Description

Bridges the CARLA server's traffic-light states into Autoware's perception output, so a CARLA
closed loop can run without camera-based traffic-light recognition.

This reworks the approach explored in #13309. The core problem is how a CARLA traffic light is
associated with an Autoware traffic_light_group_id
(the id of a traffic_light regulatory
element in the lanelet2 map). Rather than assume the CARLA OpenDRIVE signal id equals the
regulatory-element id — which only holds for maps auto-generated straight from the same OpenDRIVE —
or require a hand-written id table, the bridge discovers the mapping geometrically: each CARLA
light head is matched to the nearest lanelet2 light head and its state is published under every
regulatory element that references that head. This makes the feature work for hand-authored / TIER
IV Vector Map Builder maps, whose regulatory-element ids do not correspond to the OpenDRIVE signal
ids.

What's added

Launch arguments (all off / empty by default, so existing behavior is unchanged). Each launch
argument carries the same name as the node parameter it sets, so the settings stay grouped under
the traffic_light. namespace in ros2 param list:

launch arg / node param effect
traffic_light.publish Publish autoware_perception_msgs/TrafficLightGroupArray on /perception/traffic_light_recognition/traffic_signals every tick.
traffic_light.force_green At startup, set every CARLA traffic light to green and freeze it (camera-less closed loops that would otherwise hold at every stop line).
traffic_light.map_path lanelet2 .osm used for position matching.
traffic_light.match_distance Max head-to-head match distance (default 5.0 m).
traffic_light.match_ratio Ambiguity threshold (default 0.6).
traffic_light.id_map Optional opendrive_id:group_id[|group_id...],... override that pins a light, taking precedence over matching.

How the matching works

modules/traffic_light_matcher.py (ROS-free, unit-tested):

  • Parses the .osm directly as XML, reading each node's local_x/local_y tags — the
    Autoware map-frame coordinates (for an MGRS map, exactly the easting/northing the map loader
    produces). This avoids depending on the lanelet2 C++ regulatory-element registration and
    sidesteps any projector mismatch between this process and the map loader.
  • A physical head is a refers way, commonly shared by several regulatory elements (one per
    approaching lane / stop line), so each head carries the set of group ids that reference it; a
    matched CARLA light publishes its state under all of them.
  • Each CARLA head is expressed in the map frame from the mean of its get_light_boxes() centres
    via the existing carla_location_to_ros_point(map_origin_x, map_origin_y) transform.
  • Conservative binding. A light is bound only when a single head is clearly closest. If a head
    resolving to a different signal is nearly as close (nearest > ratio * second, the "light
    across the intersection" case), is exactly as close (a tie has no winner, so the .osm order
    must not decide it), or nothing is within match_distance, the light is left unpublished and
    logged as ambiguous / too-far — never guessed. The startup log prints a match report
    (N matched, M ambiguous, K too far); reported lights can be pinned via traffic_light.id_map.

Resolution precedence per light: traffic_light.id_map override → position match
(traffic_light.map_path) → OpenDRIVE-id-as-group-id fallback (no map path).

A malformed traffic_light.id_map entry (no :, a non-integer id, or no group id after the :)
is skipped with a warning naming it, so a typo neither takes the bridge down on the first tick nor
silently overrides a light with an empty group list.

Published states

Red/Yellow/Green map to RED/AMBER/GREEN with status SOLID_ON; CARLA's known-dark
Off maps to status SOLID_OFF, and only a state the bridge cannot interpret is published as
UNKNOWN/UNKNOWN.

How was this PR tested?

  • test/test_traffic_light_matcher.py — unit tests covering shared-head group expansion, isolated
    matches, the ambiguity guard (the disjoint-signal midpoint and the coincident-head tie that must
    be dropped, and the close same-group / clear-winner neighbours that must not), too-far, .osm
    parsing, and the traffic_light.id_map parser including its malformed-entry handling.
  • Validated the parser and matcher against the Odaiba lanelet2 map (946 heads / 906 groups): with
    realistic alignment (~0.3 m) ~97 % of heads match, and genuinely-ambiguous sub-metre distinct
    signals are reported rather than mis-assigned.
  • black (line-length 100) clean; dotted parameter names verified to declare / resolve under
    rclpy.
  • No behavioral change unless traffic_light.publish (and, for matching, traffic_light.map_path)
    is set.

@github-actions github-actions Bot added type:documentation Creating or refining documentation. (auto-assigned) component:simulation Virtual environment setups and simulations. (auto-assigned) labels Sep 4, 2026
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown

Thank you for contributing to the Autoware project!

🚧 If your pull request is in progress, switch it to draft mode.

Please ensure:

@hakuturu583
hakuturu583 marked this pull request as draft September 4, 2026 07:46
@hakuturu583
hakuturu583 force-pushed the feat/carla-traffic-light-position-match branch from 9d538e5 to 706b02c Compare September 4, 2026 07:47

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9d538e50b2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread simulator/autoware_carla_interface/src/autoware_carla_interface/carla_ros.py Outdated
@hakuturu583 hakuturu583 added the run:build-and-test-differential Mark to enable build-and-test-differential workflow. (used-by-ci) label Sep 4, 2026
Masaya Kataoka and others added 2 commits September 4, 2026 17:50
…atched to the map by position

Bridge the CARLA server's traffic-light states into Autoware's perception
output so a CARLA closed loop can run without camera-based recognition.

The key problem is associating a CARLA traffic light with an Autoware
`traffic_light_group_id` (a `traffic_light` regulatory-element id in the
lanelet2 map). Instead of assuming the CARLA OpenDRIVE signal id equals the
regulatory-element id (true only for maps auto-generated from the same
OpenDRIVE) or hand-writing an id table, the bridge discovers the mapping
geometrically: each CARLA light head is matched to the nearest lanelet2 light
head and its state is published under every regulatory element that references
that head. This works for hand-authored / Vector Map Builder maps too.

- New `modules/traffic_light_matcher.py`: parses the lanelet2 `.osm` directly
  (reads `local_x`/`local_y`, i.e. the map frame; no lanelet2/projector
  dependency), keys physical heads by their `refers` way, and matches CARLA
  heads conservatively (distance threshold + a disjoint-group ambiguity ratio),
  dropping and logging ambiguous / too-far lights rather than guessing.
- `carla_ros.py`: publishes `TrafficLightGroupArray` on
  /perception/traffic_light_recognition/traffic_signals, aggregated per group.
- `carla_autoware.py`: `force_green` freezes all lights green for camera-less
  runs.
- Parameters grouped under the `traffic_light.` namespace; resolution order is
  id-map override -> position match -> OpenDRIVE-id fallback.
- Unit tests for the matcher; README documents the feature.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Masaya Kataoka <cld-masaya.kataoka@tier4.jp>
…matching

- Use the resolved map origin (`_current_map_origin()`) instead of the raw
  `map_origin_x/y` parameters when placing CARLA light heads in the map frame,
  so georeferenced maps (origin derived from the OpenDRIVE geoReference in
  `on_world_ready`, parameters left at zero) match correctly instead of falling
  outside the distance threshold and publishing nothing. (P1)
- Treat a candidate head as a genuine alternative for the ambiguity test unless
  its group set is exactly equal to the winner's, replacing the `isdisjoint`
  check. Overlapping-but-unequal sets (e.g. {500, 501} vs {501}) would otherwise
  be accepted by arbitrary ranking and publish a missing or spurious group. Adds
  a regression test. (P2)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Masaya Kataoka <cld-masaya.kataoka@tier4.jp>
@hakuturu583
hakuturu583 force-pushed the feat/carla-traffic-light-position-match branch from 46c8693 to da0463e Compare September 4, 2026 08:51
@codecov

codecov Bot commented Sep 4, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 20.47%. Comparing base (42b76a8) to head (a459df8).
⚠️ Report is 3 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main   #13327      +/-   ##
==========================================
- Coverage   20.48%   20.47%   -0.02%     
==========================================
  Files        1948     1948              
  Lines      137253   137024     -229     
  Branches    48956    48863      -93     
==========================================
- Hits        28123    28050      -73     
+ Misses      86336    86251      -85     
+ Partials    22794    22723      -71     
Flag Coverage Δ *Carryforward flag
full-suite 20.47% <ø> (-0.02%) ⬇️ Carriedforward from d05691e

*This pull request uses carry forward flags. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@hakuturu583
hakuturu583 marked this pull request as ready for review September 4, 2026 11:42
@hakuturu583
hakuturu583 enabled auto-merge (squash) September 4, 2026 11:42

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: da0463e0ba

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread simulator/autoware_carla_interface/src/autoware_carla_interface/carla_ros.py Outdated
@Max-Bin

Max-Bin commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

@hakuturu583 Hi, thanks for the PR. Could please help solve the failed CodeScene Check?

…cene

Address the second review round:

- traffic_light.id_map now accepts several group ids per OpenDRIVE signal id,
  formatted `opendrive_id:group_id[|group_id...],...` (repeated keys merge). A
  physical head shared by multiple regulatory elements can therefore be pinned
  to all of them via one override, matching the position matcher's shared-head
  behaviour, instead of only ever recovering a single group. (Codex P2)
- Reduce complexity flagged by the CodeScene gate by extracting helpers so the
  hot methods stay flat and short:
  - traffic_light_matcher: split `load_map_traffic_lights` into node/way/relation
    parsers (Complex Method / Bumpy Road) and pull the per-head decision out of
    `match_traffic_lights` into `_classify_head` (Large Method).
  - carla_ros: split `_resolve_traffic_light_groups` into `_apply_id_map_override`
    / `_match_actors_to_map` / `_fallback_opendrive_groups`; the override parser
    is now the pure, unit-tested `parse_id_map_override`.
  - carla_autoware: fold the enable check into `_force_green_traffic_lights` so
    `load_world` keeps a single unconditional call and gains no branch.
- Extend unit tests for the multi-id override parsing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Masaya Kataoka <cld-masaya.kataoka@tier4.jp>
@hakuturu583

Copy link
Copy Markdown
Contributor Author

@Max-Bin thanks for the review! I've addressed the failing CodeScene gate in c52ece1 by refactoring the methods it flagged (rather than suppressing them):

  • traffic_light_matcher.pyload_map_traffic_lights (Complex Method / Bumpy Road) is split into small _parse_local_nodes / _parse_ways / _is_traffic_light_relation / _refers_way_ids / _way_centroid helpers, and the per-head decision is pulled out of match_traffic_lights (Large Method) into _classify_head.
  • carla_ros.py_resolve_traffic_light_groups (Complex Method / Bumpy Road) is split into _apply_id_map_override / _match_actors_to_map / _fallback_opendrive_groups.
  • carla_autoware.py — the force_green enable check is folded into _force_green_traffic_lights so load_world keeps a single unconditional call and gains no extra branch.

The same commit also addresses the Codex P2 comment (id_map now supports multiple group ids per signal). Tests still pass and black is clean; CI should re-run the CodeScene check on the new commit.

Masaya Kataoka and others added 4 commits September 7, 2026 10:12
Follow-up to the previous refactor, targeting the two rules still flagged on the
traffic-light matcher:

- Extract `_add_relation_heads` so `load_map_traffic_lights` is a single flat loop
  with no nested conditional block (clears Bumpy Road Ahead).
- Pass the per-run matching state as one `_MatchContext` namedtuple instead of a
  seven-parameter list, so `_classify_head(head, ctx)` takes two arguments (clears
  Excess Number of Function Arguments).

No behavioural change; tests still pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Masaya Kataoka <cld-masaya.kataoka@tier4.jp>
…own table)

On top of the bot autofix, finish the parts it could not:

- Capitalize the first word of the parser-helper docstrings (flake8 / pydocstyle D403).
- Order the traffic_light_matcher imports as isort (profile=black) expects.
- Remove the literal `|` from the traffic_light.id_map row of the README parameter
  table (it split the cell and broke the column count; the bot's --fix left it mangled).
  The `|`-separated multi-id syntax stays documented in prose. markdownlint and prettier
  now pass on the table.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Masaya Kataoka <cld-masaya.kataoka@tier4.jp>
Comment thread simulator/autoware_carla_interface/src/autoware_carla_interface/carla_ros.py Outdated
… the off state

Review follow-ups on the CARLA traffic-light bridge:

- traffic_light.id_map no longer lets a typo through split()/int(). A
  malformed entry ("12" with no ':', "12:" with no group id, or a
  non-integer id) is skipped with a warning naming the offending entry
  instead of raising on the first simulation tick or silently overriding
  a light with an empty group list; the other entries still apply.
  Reporting goes through an optional on_invalid callback so the parser
  stays ROS-free and unit-testable.

- The ambiguity guard now rejects an exact tie. With two heads of
  different signals at the same point both distances are 0, so
  "nearest > ratio * second" degenerates to 0 > 0 and the winner came
  down to the order the ways appear in the .osm; a tie has no winner and
  is now reported like any other ambiguous match.

- CARLA's TrafficLightState.Off is a known state (the signal is dark),
  but it was published as an UNKNOWN-color lamp with status SOLID_ON,
  i.e. "unknown color, lit". It now maps to status SOLID_OFF, and only a
  state the bridge cannot interpret stays UNKNOWN/UNKNOWN.

Tests cover the malformed-entry cases and the coincident-head tie.

Signed-off-by: Masaya Kataoka <ms.kataoka@gmail.com>

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MCAQHBrcSCmJoKn5Xi7s3k
@hakuturu583
hakuturu583 requested a review from Max-Bin September 7, 2026 14:13
…pell check

"unsignalized" is not in the Autoware spell-check dictionary and failed
spell-check-differential. Say the same thing in words the dictionary
knows; no behavior change.

Signed-off-by: Masaya Kataoka <ms.kataoka@gmail.com>

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MCAQHBrcSCmJoKn5Xi7s3k
…uity test

CodeScene flagged the previous commit for a Complex Method
(_parse_id_map_entry: two try/except blocks and a loop in one function)
and a Complex Conditional (_classify_head's compound ambiguity check).

Pull the pieces out instead of suppressing them: _parse_id (one integer
id) and _parse_id_map_groups (the group-id list) leave the entry parser
as a separator check plus two calls, and _is_ambiguous names the
runner-up test as a guarded sequence of single conditions. No behavior
change; the matcher tests still pass.

Signed-off-by: Masaya Kataoka <ms.kataoka@gmail.com>

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MCAQHBrcSCmJoKn5Xi7s3k

@Max-Bin Max-Bin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@hakuturu583
hakuturu583 merged commit 8c41273 into autowarefoundation:main Sep 8, 2026
30 checks passed
@github-project-automation github-project-automation Bot moved this from To Triage to Done in Software Working Group Sep 8, 2026
@hakuturu583
hakuturu583 deleted the feat/carla-traffic-light-position-match branch September 8, 2026 04:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

component:simulation Virtual environment setups and simulations. (auto-assigned) run:build-and-test-differential Mark to enable build-and-test-differential workflow. (used-by-ci) tag:require-cuda-build-and-test type:documentation Creating or refining documentation. (auto-assigned)

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants